package sortieren;

import javax.sound.midi.*;

/**Diese Klasse stellt einen einfachen Zugriff auf die Midi-Fähigkeiten
 * eines Rechners zur Verfügung. Eine Tonfolge ist eine Menge von Noten
 * mit einem bestimmten Anfangszeitpunkt, einer bestimmten Dauer und gespielt
 * von einem bestimmten Instrument.<p>
 * Ein Programm, das eine Tonfolge abspielen soll, belegt schon bei der
 * Erzeugung einer Tonfolge Systemresourcen (zwei Threads steuern die Soundkarte).
 * Weil eine Tonfolge mehrmals abspielbar sein soll, können diese Resourcen nach
 * dem ersten Abspielen nicht freigegeben werden. Ein Programm mit nicht beendeten
 * Threads endet aber nicht gewöhnlich, sondern muss mit <CODE>System.exit(0)</CODE>
 * beendet werden. Damit werden dann die beiden erwähnten Threads auch getötet.<p>
 * Wenn dieses (logische) Verhalten nicht gewünscht wird, weil etwa eine Tonfolge
 * nur ein einziges Mal abgespielt und die Geschichte damit beendet sein soll,
 * kann mit der Methode {@link endNote()} eine Abbruchnachricht in die Tonsequenz
 * eingeschoben werden. Diese sorgt dafür, dass die beiden Threads am Ende getötet
 * werden. Die Tonfolge ist danach aber nicht weiter zu gebrauchen. Da solch ein
 * Verhalten unüblich ist, ist die Methode endNote eigentlich eine technische
 * Schweinerei und deshalb <B>deprecated</B>.<p>
 * Nähere Informationen zur Tonerzeugung findet man in doc/guide/sound.
 */
public class Tonfolge implements MetaEventListener{
  private Track track;
  private Sequencer sequencer;
  private Sequence sequence;
  private MetaMessage stopmessage=new MetaMessage();
  /**Gibt die zeitlich hinterste Position des Liedes an.*/
  private int lastTick;

  /**Erzeugt eine Tonfolge.*/
  public Tonfolge(){
    try{
      stopmessage.setMessage(47, null, 0);//47=MetaMessage: Liedende
      sequencer=MidiSystem.getSequencer();
      sequence=new Sequence(Sequence.PPQ, 4);
    }
    catch(Exception e){e.printStackTrace();}
    sequencer.addMetaEventListener(this);
    track=sequence.createTrack();
  }

  /**Löscht den Inhalt der Tonfolge.*/
  public void clear(){
    if(track!=null) sequence.deleteTrack(track);
    track=sequence.createTrack();
  }

  /**Spielt die Tonfolge mit einer bestimmten Geschwindigkeit ab. Die Angabe
   * erfolgt in Beats per Minute. Ein Beat ist vier Ticks lang.
   * @param bpm die Geschwindigkeit in Beats per Minute.
   */
  public void start(final int bpm) {
    try{
      sequencer.open();
      sequencer.setSequence(sequence);
    } catch(Exception e){e.printStackTrace();}
    sequencer.setTempoInBPM(bpm);
    sequencer.start();
  }

  public void stop(){
    if(sequencer.isRunning()) sequencer.stop();
    if(sequencer.isOpen()) sequencer.close();
  }

  /**Gibt an, ob die Tonfolge gerade im Begriff ist, abgespielt zu werden.
   * @return true, wenn noch Töne zu spielen sind, sonst false.
   */
  public boolean dudelt(){
    return sequencer.isRunning();
  }

  /**Wartet, bis die Tonfolge fertig abgespielt ist.*/
  public void join(){
    while(dudelt())
      try{Thread.sleep(100);} catch(InterruptedException ex){}
  }

  /**Erzeugt eine neue Note.
   * @param note die Nummer der gewünschten Note. 60 steht für das
   * ein-gestrichene c. Jede Zahl steht für einen Halbton.
   * @param vonTick die Startzeit der Note in Ticks ab dem Anfang der Folge.
   * @param lenTick die Länge der Note in Ticks. 4 Ticks sind ein Beat (Takt).
   */
  public void neueNote(int note, int vonTick, int lenTick){
    startNote(note, vonTick);
    stopNote(note, vonTick+lenTick);
  }

  /**Schlägt eine Note an.
   * @param note die Nummer der gewünschten Note.
   * @param tick die Startzeit der Note ab dem Anfang der Folge in
   * Ticks. 4 Ticks sind ein Beat (Takt).
   */
  public void startNote(int note, int tick) {
    setShortMessage(ShortMessage.NOTE_ON, note, tick);
  }

  /**Beendet eine Note.
   * @param note die Nummer der zu beendenden Note.
   * @param tick die Zeit des Endes in Ticks ab dem Start der Folge.
   */
  public void stopNote(int note, int tick) {
    setShortMessage(ShortMessage.NOTE_OFF, note, tick);
    if(lastTick<=tick) lastTick=tick;
  }

  /**Hängt eine Note an das Ende der Folge.
   * @param note die Nummer der Note.
   * @param lenTick die Länge der Note in Ticks.
   */
  public void hintenNote(int note, int lenTick){
    neueNote(note, lastTick, lenTick);
  }
  
  /**Hängt hinten eine Pause an.
   *@param lenTick die Länge der Pause in Ticks.
   */
  public void hintenPause(int lenTick){
    lastTick+=lenTick;
  }

  /**Gibt die Länge des Liedes an.
   *return die Tickposition nach dem letzten Ton.
   */
  public int lastTick(){return lastTick;}
  
  /**Schaltet um auf ein neues Instrument. Die Zuordnungen der Instrumente
   * zu den positiven Zahlen folgt der Midi-Spezifikation.
   * @param instr die Nummer des Instruments.
   */
  public void instrument(int instr){
    setShortMessage(ShortMessage.PROGRAM_CHANGE,instr,lastTick);
  }

  private void setShortMessage(int befehl, int note, int tick){
    ShortMessage message=new ShortMessage();
    try{
      message.setMessage(befehl, 0, note, 90);
      MidiEvent event=new MidiEvent(message, tick);
      track.add(event);
    } catch(InvalidMidiDataException e) {
      e.printStackTrace();
    }
  }

  /**Diese Methode muss einmal am Ende des Stückes aufgerufen werden, um
   * eine end-Markierung im Lied zu setzen. Dadurch kann dann die Tonwiedergabe
   * beendet werden. Wird eine Note zeitlich nach der Endnote gesetzt, so wird
   * sie beim Abspielen nicht wiedergegeben.
   * @deprecated Eine Tonfolge ist bei Verwendung dieser Methode nur ein einziges Mal
   * abspielbar. Ein nachträgliches Bearbeiten der Töne hat also keinen Sinn.
   * Diese Methode zu verwenden hat nur dann Sinn, wenn das verwendende Programm
   * ein Konsolenprogramm ist, das nach der Abarbeitung der main-Methode
   * terminieren soll, dies aber auf Grund der hier erzeugten Threads nicht
   * tun würde.
   */
  public void endNote(){
    MidiEvent event=new MidiEvent(stopmessage, lastTick+2);
    track.add(event);
  }

  /**Wird aufgerufen, wenn in der Tonfolge ein MetaEvent passiert, also etwas, das
   * mit der Musik nichts zu tun hat. Die Methode tötet dann alle Threads. Ein
   * Programm, das eine <CODE>Tonfolge</CODE> verwendet, endet dann auf gewöhnlichem
   * Weg. Dies ist nur dann von Belang, wenn es sich um ein Konsolenprogramm handelt.
   * Programme mit GUI enden sowieso nicht gewöhnlich.
   * @param mm die MetaMessage. Tonfolge reagiert nur auf Typ 47 (Abbruch).
   */  
  @Override
  public void meta(MetaMessage mm){
    if(mm.getType()!=47) return;
    sequencer.close();
    ThreadGroup g=Thread.currentThread().getThreadGroup();
    Thread[] alle=new Thread[g.activeCount()];
    g.enumerate(alle);
    for(Thread t: alle)
      if(t.getName().toLowerCase().contains("sound")) t.interrupt();
  }

  /* Die folgende main-Methode ist nur deswegen da, damit die Klasse
   * einen Ton produziert, wenn man sie selber aufruft. Das ist aber
   * nicht der Sinn ihrer Verwendung.
  */
  public static void main(String[] args){
    Tonfolge tf=new Tonfolge();
    tf.instrument(2);
    tf.hintenNote(60,1);
    tf.instrument(7);
    tf.hintenNote(60,1);
    int da=tf.lastTick;
    tf.instrument(42);
    tf.neueNote(65,da,4);
    tf.neueNote(67,da,4);
    tf.neueNote(70,da,4);
    tf.start(50);
    while(tf.dudelt());//leere Schleife wartet auf das Ende des Liedes
    //nun soll die Tonfolge erweitert und nochmal abgespielt werden.
    tf.instrument(60);
    tf.hintenNote(70, 6);
    tf.endNote();//dieses ist ein Konsolenprogramm und soll selber terminieren
    tf.start(50);
  }
}
